package com.avcompris.util;

import static com.avcompris.util.DateUtils.DATETIMEFORMATTER_COLON_MS_Z;
import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static com.avcompris.util.XMLUtils.createAttributes;
import static org.apache.commons.io.FileUtils.openInputStream;
import static org.apache.commons.lang3.CharEncoding.UTF_8;
import static org.apache.commons.lang3.CharUtils.CR;
import static org.apache.commons.lang3.CharUtils.LF;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.joda.time.DateTime;
import org.jvyaml.ParserException;
import org.jvyaml.ScannerException;
import org.jvyaml.YAML;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.lang.NotImplementedException;

/**
 * utility class for YAML manipulation.
 * 
 * @author David Andriana Copyright Avantage Compris SARL 2008-2009 ©
 */
public abstract class YamlUtils extends AbstractUtils {

	private static final String CRLF = "" + CR + LF;

	/**
	 * load a YAML file with different streams, into an array of {@link YAML}
	 * objects.
	 * 
	 * @param yamlFile
	 *            the YAML input file
	 * @return the loaded {@link YAML} objects.
	 */
	@Nullable
	public static Object[] loadAllYAML(final File yamlFile) throws IOException {

		nonNullArgument(yamlFile, "yamlFile");

		final InputStream is = openInputStream(yamlFile);
		try {

			final BufferedReader br = new BufferedReader(new InputStreamReader(
					is, UTF_8));

			return loadAllYAML(br);

		} finally {
			is.close();
		}
	}

	private static Object[] loadAllYAML(final BufferedReader br)
			throws IOException {

		nonNullArgument(br, "bufferedReader");

		final List<Object> yamls = new ArrayList<Object>();

		final StringBuilder sb = new StringBuilder();

		boolean started = false;

		while (true) {

			final String line = br.readLine();

			if (line == null) {

				break;
			}

			if (started) {

				if (line.startsWith("---")) {

					final Object yaml = loadYAML(sb.toString());

					yamls.add(yaml);

					sb.setLength(0);

					started = false;

				} else {

					sb.append(line).append(CRLF);
				}

			} else if (line.startsWith("---") || "".equals(line.trim())) {

				continue;

			} else {

				started = true;

				sb.append(line).append(CRLF);
			}

		}

		if (started) {

			final Object yaml = loadYAML(sb.toString());

			yamls.add(yaml);

			sb.setLength(0);

			started = false;
		}

		return yamls.toArray();
	}

	/**
	 * load a YAML file into a {@link YAML} object.
	 * 
	 * @param yamlFile
	 *            the YAML input file
	 * @return the loaded {@link YAML} object.
	 */
	@Nullable
	public static Yamled loadYAML(final File yamlFile) throws IOException {

		nonNullArgument(yamlFile, "yamlFile");

		final Object yaml;

		final InputStream is = openInputStream(yamlFile);
		try {

			final Reader reader = new InputStreamReader(is, UTF_8);

			try {

				yaml = YAML.load(reader);

			} catch (final ParserException e) {

				throw new RuntimeException("Could not parse YAML file: "
						+ yamlFile.getCanonicalPath(), e);

			} catch (final ScannerException e) {

				throw new RuntimeException("Error while parsing YAML File: "
						+ yamlFile.getCanonicalPath(), e);
			}

		} finally {
			is.close();
		}

		return YamledImpl.wrapToYamled(yaml);
	}

	/**
	 * load a YAML resource with different streams, into an array of
	 * {@link YAML} objects.
	 * 
	 * @param classLoader
	 *            the {@link ClassLoader} to use to read the resource
	 * @param yamlResource
	 *            the YAML resource file to read
	 * @return the loaded {@link YAML} objects.
	 */
	public static Object[] loadAllYAMLResource(final ClassLoader classLoader,
			final String yamlResource) throws IOException {

		nonNullArgument(classLoader, "classLoader");
		nonNullArgument(yamlResource, "yamlResource");

		throw new NotImplementedException();
	}

	/**
	 * load a YAML resource into a {@link YAML} object.
	 * 
	 * @param classLoader
	 *            the {@link ClassLoader} to use to read the resource
	 * @param yamlResource
	 *            the YAML resource file to read
	 * @return the loaded {@link YAML} object.
	 */
	public static Object loadYAMLResource(final ClassLoader classLoader,
			final String yamlResource) throws IOException {

		nonNullArgument(classLoader, "classLoader");
		nonNullArgument(yamlResource, "yamlResource");

		final Object yaml;

		final InputStream is = classLoader.getResourceAsStream(yamlResource);

		if (is == null) {
			throw new NullPointerException("Cannot find YAML resource: "
					+ yamlResource);
		}
		try {

			final Reader reader = new InputStreamReader(is, UTF_8);

			try {

				yaml = YAML.load(reader);

			} catch (final ParserException e) {

				throw new RuntimeException("Could not parse YAML resource: "
						+ yamlResource, e);

			} catch (final ScannerException e) {

				throw new RuntimeException(
						"Error while parsing YAML resource: " + yamlResource, e);
			}

		} finally {
			is.close();
		}

		return yaml;
	}

	/**
	 * load a YAML resource with different streams, into an array of
	 * {@link YAML} objects.
	 * 
	 * @param c
	 *            the {@link Class} from which to read the resource
	 * @param yamlResource
	 *            the YAML resource file to read
	 * @return the loaded {@link YAML} objects.
	 */
	public static Object[] loadAllYAMLResource(final Class<?> c,
			final String yamlResource) throws IOException {

		nonNullArgument(c, "class");

		return loadAllYAMLResource(c.getClassLoader(), yamlResource);
	}

	/**
	 * load a YAML resource into a {@link YAML} object.
	 * 
	 * @param c
	 *            the {@link Class} from which to read the resource
	 * @param yamlResource
	 *            the YAML resource file to read
	 * @return the loaded {@link YAML} object.
	 */
	public static Object loadYAMLResource(final Class<?> c,
			final String yamlResource) throws IOException {

		nonNullArgument(c, "class");

		return loadYAMLResource(c.getClassLoader(), yamlResource);
	}

	/**
	 * load a YAML {@link String} into a {@link YAML} object.
	 * 
	 * @param text
	 *            the YAML input content
	 * @return the loaded {@link YAML} object.
	 */
	@Nullable
	public static Object loadYAML(final String text) throws IOException {

		nonNullArgument(text, "text");

		final Object yaml;

		final Reader reader = new StringReader(text);

		try {

			yaml = YAML.load(reader);

		} catch (final ScannerException e) {

			throw new RuntimeException("Error while parsing YAML text", e);
		}

		return yaml;
	}

	/**
	 * load a YAML {@link String} with different streams, into an array of
	 * {@link YAML} objects.
	 * 
	 * @param text
	 *            the YAML input content
	 * @return the loaded {@link YAML} objects.
	 */
	@Nullable
	public static Object[] loadAllYAML(final String text) throws IOException {

		nonNullArgument(text, "text");

		final BufferedReader br = new BufferedReader(new StringReader(text));

		return loadAllYAML(br);
	}

	/**
	 * transform a YAML object loaded with Jvyaml into a XML stream.
	 * 
	 * @param yaml
	 *            the YAML object, loaded with Jvyaml
	 * @param contentHandler
	 *            the SAX {@link ContentHandler} that will receive the SAX
	 *            events
	 */
	public static void yaml2xml(@Nullable final Object yaml,
			final ContentHandler contentHandler) throws SAXException {

		nonNullArgument(contentHandler, "contentHandler");

		contentHandler.startDocument();

		subYaml2xml(yaml, contentHandler);

		contentHandler.endDocument();
	}

	/**
	 * transform a YAML object loaded with Jvyaml into a XML stream.
	 * 
	 * @param yaml
	 *            the YAML object, loaded with Jvyaml
	 * @return the resulting XML content
	 */
	public static String yaml2xml(@Nullable final Object yaml)
			throws SAXException {

		final Writer writer = new StringWriter();

		final ContentHandler contentHandler = new AvcXMLSerializer(writer);

		yaml2xml(yaml, contentHandler);

		final String s = writer.toString();

		return s;
	}

	/**
	 * transform a YAML sub-structure loaded with Jvyaml into a XML stream.
	 * 
	 * @param yaml
	 *            the YAML sub-structure, loaded with Jvyaml
	 * @param contentHandler
	 *            the SAX {@link ContentHandler} that will receive the SAX
	 *            events
	 */
	private static void subYaml2xml(@Nullable final Object yaml,
			final ContentHandler contentHandler) throws SAXException {

		nonNullArgument(contentHandler, "contentHandler");

		if (yaml == null) {

			contentHandler.startElement(null, "map", null, null);

			contentHandler.endElement(null, "map", null);

			return;
		}

		final Class<?> yamlClass = yaml.getClass();

		final String className = yamlClass.getName();

		if (yaml instanceof Map<?, ?>) {

			final Map<?, ?> map = (Map<?, ?>) yaml;

			contentHandler.startElement(null, "map", null, null);

			final Set<?> keys = map.keySet();

			for (final Object key : new TreeSet<Object>(keys)) {

				contentHandler.startElement(null, "pair", null, null);

				contentHandler.startElement(null, "key", null, null);

				subYaml2xml(key, contentHandler);

				contentHandler.endElement(null, "key", null);

				final Object value = map.get(key);

				contentHandler.startElement(null, "value", null, null);

				subYaml2xml(value, contentHandler);

				contentHandler.endElement(null, "value", null);

				contentHandler.endElement(null, "pair", null);
			}

			contentHandler.endElement(null, "map", null);

		} else if (yaml instanceof List<?>) {

			final Collection<?> list = (Collection<?>) yaml;

			contentHandler.startElement(null, "list", null, null);

			for (final Object item : list) {

				contentHandler.startElement(null, "item", null, null);

				subYaml2xml(item, contentHandler);

				contentHandler.endElement(null, "item", null);
			}

			contentHandler.endElement(null, "list", null);

		} else if (yaml instanceof String) {

			final String s = (String) yaml;

			contentHandler.startElement(null, "scalar", null,
					createAttributes("type", "string"));

			contentHandler.characters(s.toCharArray(), 0, s.length());

			contentHandler.endElement(null, "scalar", null);

		} else if (yaml instanceof Integer) {

			final Integer n = (Integer) yaml;

			final String s = Integer.toString(n);

			contentHandler.startElement(null, "scalar", null,
					createAttributes("type", "int"));

			contentHandler.characters(s.toCharArray(), 0, s.length());

			contentHandler.endElement(null, "scalar", null);

		} else if (yaml instanceof Long) {

			final Long n = (Long) yaml;

			final String s = Long.toString(n);

			contentHandler.startElement(null, "scalar", null,
					createAttributes("type", "long"));

			contentHandler.characters(s.toCharArray(), 0, s.length());

			contentHandler.endElement(null, "scalar", null);

		} else if (yaml instanceof Double) {

			final Double d = (Double) yaml;

			final String s = Double.toString(d);

			contentHandler.startElement(null, "scalar", null,
					createAttributes("type", "double"));

			contentHandler.characters(s.toCharArray(), 0, s.length());

			contentHandler.endElement(null, "scalar", null);

		} else if (yaml instanceof Boolean) {

			final Boolean b = (Boolean) yaml;

			final String s = Boolean.toString(b);

			contentHandler.startElement(null, "scalar", null,
					createAttributes("type", "boolean"));

			contentHandler.characters(s.toCharArray(), 0, s.length());

			contentHandler.endElement(null, "scalar", null);

		} else if (yaml instanceof Date) {

			final Date date = (Date) yaml;

			final DateTime dateTime = new DateTime(date.getTime());

			final String s = dateTime.toString(DATETIMEFORMATTER_COLON_MS_Z);

			contentHandler.startElement(null, "scalar", null,
					createAttributes("type", "date"));

			contentHandler.characters(s.toCharArray(), 0, s.length());

			contentHandler.endElement(null, "scalar", null);

		} else {

			throw new IllegalArgumentException("Unknown YAML type: "
					+ className + " [" + yaml + "]");
		}
	}

	/**
	 * retrieve a property from a YAML map.
	 * 
	 * @param <T>
	 *            the class that represents the <tt>type</tt> passed as
	 *            parameter
	 * @param type
	 *            the required type
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @param propertyName
	 *            the property's name
	 * @return the property's value
	 */
	public static <T> T getProperty(final Class<T> type, final Object yaml,
			final Object propertyName) {

		nonNullArgument(type, "type");
		nonNullArgument(yaml, "yaml");
		nonNullArgument(propertyName, "propertyName");

		if (!(yaml instanceof Map<?, ?>)) {
			throw new IllegalArgumentException(
					"yaml should be an instance of Map<?, ?>: "
							+ yaml.getClass().getName());
		}

		final Map<?, ?> map = (Map<?, ?>) yaml;

		final Object value = map.get(propertyName);

		if (value == null) {
			return null;
		}

		if (!type.isInstance(value)) {

			if (type.equals(String.class)) {
				return type.cast(value.toString());
			}

			throw new ClassCastException("Property value for \"" + propertyName
					+ "\" is not an instance of " + type.getName() + ": "
					+ value.getClass().getName());
		}

		return type.cast(value);
	}

	/**
	 * retrieve the property names from a YAML map.
	 * 
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @return the property names
	 */
	public static String[] getPropertyNames(final Object yaml) {

		nonNullArgument(yaml, "yaml");

		if (!(yaml instanceof Map<?, ?>)) {
			throw new IllegalArgumentException(
					"yaml should be an instance of Map<?, ?>: "
							+ yaml.getClass().getName());
		}

		final Map<?, ?> map = (Map<?, ?>) yaml;

		final String[] names = new String[map.size()];

		int i = 0;

		for (final Object key : map.keySet()) {

			if (!(key instanceof String)) {
				throw new ClassCastException("Property name #" + i
						+ " is not an instance of String: " + key);
			}

			names[i] = (String) key;

			++i;
		}

		return names;
	}

	/**
	 * retrieve the property keys (names or other types) from a YAML map.
	 * 
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @return the property names
	 */
	public static Object[] getPropertyKeys(final Object yaml) {

		nonNullArgument(yaml, "yaml");

		if (!(yaml instanceof Map<?, ?>)) {
			throw new IllegalArgumentException(
					"yaml should be an instance of Map<?, ?>: "
							+ yaml.getClass().getName());
		}

		final Map<?, ?> map = (Map<?, ?>) yaml;

		final Object[] keys = new Object[map.size()];

		int i = 0;

		for (final Object key : map.keySet()) {

			keys[i] = key;

			++i;
		}

		return keys;
	}

	/**
	 * retrieve a {@link String} property from a YAML map.
	 * 
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @param propertyName
	 *            the property's name
	 * @return the property's value
	 */
	public static String getStringProperty(final Object yaml,
			final String propertyName) {

		return getProperty(String.class, yaml, propertyName);
	}

	/**
	 * retrieve a {@link Boolean} property from a YAML map.
	 * 
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @param propertyName
	 *            the property's name
	 * @return the property's value
	 */
	public static Boolean getBooleanProperty(final Object yaml,
			final String propertyName) {

		return getProperty(Boolean.class, yaml, propertyName);
	}

	/**
	 * retrieve a {@link List} property from a YAML map.
	 * 
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @param propertyName
	 *            the property's name
	 * @return the property's value
	 */
	public static Collection<?> getListProperty(final Object yaml,
			final String propertyName) {

		return getProperty(List.class, yaml, propertyName);
	}

	/**
	 * retrieve a {@link Object} property from a YAML map.
	 * 
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @param propertyName
	 *            the property's name
	 * @return the property's value
	 */
	public static Object getObjectProperty(final Object yaml,
			final String propertyName) {

		return getProperty(Object.class, yaml, propertyName);
	}

	/**
	 * retrieve a {@link Object} property from a YAML map.
	 * 
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @param propertyKey
	 *            the property's key
	 * @return the property's value
	 */
	public static Object getObjectProperty(final Object yaml,
			final Object propertyKey) {

		return getProperty(Object.class, yaml, propertyKey);
	}

	/**
	 * retrieve the unique key of a YAML map.
	 * 
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @return the key
	 */
	public static String getKey(final Object yaml) {

		final Map<?, ?> map = getUniqueEntryMap(yaml);

		return (String) map.keySet().iterator().next();
	}

	/**
	 * retrieve the map with a unique entry of a YAML map.
	 * 
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @return the same YAML object, but verified and cast as a map
	 */
	private static Map<?, ?> getUniqueEntryMap(final Object yaml) {

		nonNullArgument(yaml, "yaml");

		if (!(yaml instanceof Map<?, ?>)) {
			throw new IllegalArgumentException(
					"yaml should be an instance of Map<?, ?>: "
							+ yaml.getClass().getName());
		}

		final Map<?, ?> map = (Map<?, ?>) yaml;

		if (map.size() != 1) {
			throw new IllegalArgumentException(
					"yaml should be a Map<?, ?> of size 1, but was: "
							+ map.size());
		}
		return map;
	}

	/**
	 * retrieve the unique value of a YAML map.
	 * 
	 * @param yaml
	 *            the YAML object (must be a map)
	 * @return the value
	 */
	public static Object getValue(final Object yaml) {

		final Map<?, ?> map = getUniqueEntryMap(yaml);

		return map.values().iterator().next();
	}
}
